# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#


"""
PyInstaller build script (Python version)
"""

import os
import shutil
import subprocess
import sys
from pathlib import Path


def get_venv_base_dir():
    """
    Get the base directory for virtual environments outside the project.
    
    Returns:
        Path: Base directory path
        - Linux/macOS: ~/.cache/iotdb-ainode-build/
        - Windows: %LOCALAPPDATA%\\iotdb-ainode-build\\
    """
    if sys.platform == "win32":
        localappdata = os.environ.get("LOCALAPPDATA") or os.environ.get(
            "APPDATA", os.path.expanduser("~")
        )
        base_dir = Path(localappdata) / "iotdb-ainode-build"
    else:
        base_dir = Path.home() / ".cache" / "iotdb-ainode-build"

    return base_dir


def setup_venv():
    """
    Create virtual environment outside the project directory.
    
    The virtual environment is created in a platform-specific location:
    - Linux/macOS: ~/.cache/iotdb-ainode-build/<project-name>/
    - Windows: %LOCALAPPDATA%\\iotdb-ainode-build\\<project-name>\\
    
    The same venv is reused across multiple builds of the same project.
    
    Returns:
        Path: Path to the virtual environment directory
    """
    script_dir = Path(__file__).parent
    venv_base_dir = get_venv_base_dir()
    venv_dir = venv_base_dir / script_dir.name

    if venv_dir.exists():
        print(f"Virtual environment already exists at: {venv_dir}")
        return venv_dir

    venv_base_dir.mkdir(parents=True, exist_ok=True)
    print(f"Creating virtual environment at: {venv_dir}")
    subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True)
    print("Virtual environment created successfully")
    return venv_dir


def get_venv_python(venv_dir):
    """Get Python executable path in virtual environment"""
    if sys.platform == "win32":
        return venv_dir / "Scripts" / "python.exe"
    else:
        return venv_dir / "bin" / "python"


def update_pip(venv_python):
    """Update pip in the virtual environment to the latest version."""
    print("Updating pip...")
    subprocess.run(
        [str(venv_python), "-m", "pip", "install", "--upgrade", "pip"], check=True
    )
    print("pip updated successfully")


def install_poetry(venv_python):
    """Install poetry 2.2.1 in the virtual environment."""
    print("Installing poetry 2.2.1...")
    subprocess.run(
        [
            str(venv_python),
            "-m",
            "pip",
            "install",
            "poetry==2.2.1",
        ],
        check=True,
    )
    # Get installed version
    version_result = subprocess.run(
        [str(venv_python), "-m", "poetry", "--version"],
        capture_output=True,
        text=True,
        check=True,
    )
    print(f"Poetry installed: {version_result.stdout.strip()}")


def get_venv_env(venv_dir):
    """
    Get environment variables configured for the virtual environment.

    Sets VIRTUAL_ENV and prepends the venv's bin/Scripts directory to PATH
    so that tools installed in the venv take precedence.
    Also sets POETRY_VIRTUALENVS_PATH to force poetry to use our venv.

    Returns:
        dict: Environment variables dictionary
    """
    env = os.environ.copy()
    env["VIRTUAL_ENV"] = str(venv_dir.absolute())

    venv_bin = str(venv_dir / ("Scripts" if sys.platform == "win32" else "bin"))
    env["PATH"] = f"{venv_bin}{os.pathsep}{env.get('PATH', '')}"

    # Force poetry to use our virtual environment by setting POETRY_VIRTUALENVS_PATH
    # This tells poetry where to look for/create virtual environments
    env["POETRY_VIRTUALENVS_PATH"] = str(venv_dir.parent.absolute())

    return env


def get_poetry_executable(venv_dir):
    """Get poetry executable path in the virtual environment."""
    if sys.platform == "win32":
        return venv_dir / "Scripts" / "poetry.exe"
    else:
        return venv_dir / "bin" / "poetry"


def install_dependencies(venv_python, venv_dir, script_dir):
    """
    Install project dependencies using poetry.

    Configures poetry to use the external virtual environment and installs
    all dependencies from pyproject.toml.
    """
    print("Installing dependencies with poetry...")
    venv_env = get_venv_env(venv_dir)
    poetry_exe = get_poetry_executable(venv_dir)

    # Configure poetry settings
    print("Configuring poetry settings...")
    try:
        # Set poetry to not create venvs in project directory
        subprocess.run(
            [str(poetry_exe), "config", "virtualenvs.in-project", "false"],
            cwd=str(script_dir),
            env=venv_env,
            check=True,
            capture_output=True,
            text=True,
        )
        # Set poetry virtualenvs path to our venv directory's parent
        # This forces poetry to look for/create venvs in the same location as our venv
        subprocess.run(
            [
                str(poetry_exe),
                "config",
                "virtualenvs.path",
                str(venv_dir.parent.absolute()),
            ],
            cwd=str(script_dir),
            env=venv_env,
            check=True,
            capture_output=True,
            text=True,
        )
        # Ensure poetry can use virtual environments
        subprocess.run(
            [str(poetry_exe), "config", "virtualenvs.create", "true"],
            cwd=str(script_dir),
            env=venv_env,
            check=True,
            capture_output=True,
            text=True,
        )
    except Exception as e:
        print(f"Warning: Failed to configure poetry settings: {e}")
        # Continue anyway, as these may not be critical

    # Remove any existing poetry virtual environments for this project
    # This ensures poetry will use our specified virtual environment
    print("Removing any existing poetry virtual environments...")
    remove_result = subprocess.run(
        [str(poetry_exe), "env", "remove", "--all"],
        cwd=str(script_dir),
        env=venv_env,
        check=False,  # Don't fail if no venv exists
        capture_output=True,
        text=True,
    )
    if remove_result.stdout:
        print(remove_result.stdout.strip())
    if remove_result.stderr:
        stderr = remove_result.stderr.strip()
        # Ignore "No virtualenv has been activated" error
        if "no virtualenv" not in stderr.lower():
            print(remove_result.stderr.strip())

    # Verify the virtual environment Python is valid before configuring poetry
    print(f"Verifying virtual environment Python at: {venv_python}")
    if not venv_python.exists():
        print(f"ERROR: Virtual environment Python not found at: {venv_python}")
        sys.exit(1)

    python_version_result = subprocess.run(
        [str(venv_python), "--version"],
        capture_output=True,
        text=True,
        check=False,
    )
    if python_version_result.returncode != 0:
        print(f"ERROR: Virtual environment Python is not executable: {venv_python}")
        sys.exit(1)
    print(f"  Python version: {python_version_result.stdout.strip()}")

    # Instead of using poetry env use (which creates new venvs), we'll use a different approach:
    # 1. Create a symlink from poetry's expected venv location to our venv
    # 2. Or, directly use poetry install with VIRTUAL_ENV set (poetry should detect it)
    #
    # The issue is that poetry env use creates venvs with hash-based names in its cache.
    # We need to work around this by either:
    # - Creating a symlink from poetry's expected location to our venv
    # - Or bypassing poetry env use entirely and using poetry install directly

    # Strategy: Create a symlink from poetry's expected venv location to our venv
    # Poetry creates venvs with names like: <project-name>-<hash>-py<python-version>
    # We need to find out what poetry would name our venv, then create a symlink

    print(f"Configuring poetry to use virtual environment at: {venv_dir}")

    # Get poetry's expected venv name by checking what it would create
    # First, let's try poetry env use, but catch if it tries to create a new venv
    result = subprocess.run(
        [str(poetry_exe), "env", "use", str(venv_python)],
        cwd=str(script_dir),
        env=venv_env,
        check=False,
        capture_output=True,
        text=True,
    )

    output_text = (result.stdout or "") + (result.stderr or "")

    # If poetry is creating a new venv, we need to stop it and use a different approach
    if (
        "Creating virtualenv" in output_text
        or "Creating virtual environment" in output_text
        or "Using virtualenv:" in output_text
    ):
        print("Poetry is attempting to create/use a new virtual environment.")
        print(
            "Stopping this and using alternative approach: creating symlink to our venv..."
        )

        # Extract the venv path poetry is trying to create/use
        # Look for patterns like "Using virtualenv: /path/to/venv" or "Creating virtualenv name in /path"
        import re

        poetry_venv_path = None

        # Try to extract from "Using virtualenv: /path/to/venv"
        using_match = re.search(r"Using virtualenv:\s*([^\s\n]+)", output_text)
        if using_match:
            poetry_venv_path = Path(using_match.group(1))

        # If not found, try to extract from "Creating virtualenv name in /path"
        if not poetry_venv_path:
            creating_match = re.search(
                r"Creating virtualenv[^\n]*in\s+([^\s\n]+)", output_text
            )
            if creating_match:
                venv_dir_path = Path(creating_match.group(1))
                # Extract venv name from the output
                name_match = re.search(r"Creating virtualenv\s+([^\s]+)", output_text)
                if name_match:
                    venv_name = name_match.group(1)
                    poetry_venv_path = venv_dir_path / venv_name

        # If still not found, try to find any path in pypoetry/virtualenvs
        if not poetry_venv_path:
            pypoetry_match = re.search(
                r"([^\s]+pypoetry[^\s]*virtualenvs[^\s]+)", output_text
            )
            if pypoetry_match:
                poetry_venv_path = Path(pypoetry_match.group(1))

        if poetry_venv_path:
            print(f"Poetry wants to create/use venv at: {poetry_venv_path}")

            # Remove the venv poetry just created (if it exists)
            if poetry_venv_path.exists() and poetry_venv_path.is_dir():
                print(f"Removing poetry's newly created venv: {poetry_venv_path}")
                shutil.rmtree(poetry_venv_path, ignore_errors=True)

            # Create a symlink from poetry's expected location to our venv
            print(f"Creating symlink from {poetry_venv_path} to {venv_dir}")
            try:
                if poetry_venv_path.exists() or poetry_venv_path.is_symlink():
                    if poetry_venv_path.is_symlink():
                        poetry_venv_path.unlink()
                    elif poetry_venv_path.is_dir():
                        shutil.rmtree(poetry_venv_path, ignore_errors=True)
                poetry_venv_path.parent.mkdir(parents=True, exist_ok=True)
                poetry_venv_path.symlink_to(venv_dir)
                print(f"✓ Symlink created successfully")
            except Exception as e:
                print(f"WARNING: Failed to create symlink: {e}")
                print("Will try to use poetry install directly with VIRTUAL_ENV set")
        else:
            print("Could not determine poetry's venv path from output")
            print(f"Output was: {output_text}")
    else:
        if result.stdout:
            print(result.stdout.strip())
        if result.stderr:
            stderr = result.stderr.strip()
            if stderr:
                print(f"Poetry output: {stderr}")

    # Verify poetry is using the correct virtual environment BEFORE running lock/install
    # This is critical - if poetry uses the wrong venv, dependencies won't be installed correctly
    print("Verifying poetry virtual environment...")

    # Wait a moment for symlink to be recognized (if we created one)
    import time

    time.sleep(0.5)

    verify_result = subprocess.run(
        [str(poetry_exe), "env", "info", "--path"],
        cwd=str(script_dir),
        env=venv_env,
        check=False,  # Don't fail if poetry hasn't activated a venv yet
        capture_output=True,
        text=True,
    )

    expected_venv_path_resolved = str(Path(venv_dir.absolute()).resolve())

    # If poetry env info fails, it might mean poetry hasn't activated the venv yet
    if verify_result.returncode != 0:
        print(
            "Warning: poetry env info failed, poetry may not have activated the virtual environment yet"
        )
        print(
            "This may be okay if we created a symlink - poetry should use it when running commands"
        )
        poetry_venv_path_resolved = None
    else:
        poetry_venv_path = verify_result.stdout.strip()

        # Normalize paths for comparison (resolve symlinks, etc.)
        poetry_venv_path_resolved = str(Path(poetry_venv_path).resolve())

    # Only verify path if we successfully got poetry's venv path
    if poetry_venv_path_resolved is not None:
        if poetry_venv_path_resolved != expected_venv_path_resolved:
            print(
                f"ERROR: Poetry is using {poetry_venv_path}, but expected {expected_venv_path_resolved}"
            )
            print(
                "Poetry must use the virtual environment we created for the build to work correctly."
            )
            print("The symlink approach may not have worked. Please check the symlink.")
            sys.exit(1)
        else:
            print(
                f"✓ Poetry is correctly using virtual environment: {poetry_venv_path}"
            )
    else:
        print("Warning: Could not verify poetry virtual environment path")
        print(
            "Continuing anyway - poetry should use the venv via symlink or VIRTUAL_ENV"
        )

    # Update lock file and install dependencies
    # Re-verify environment before each command to ensure poetry doesn't switch venvs
    def verify_poetry_env():
        verify_result = subprocess.run(
            [str(poetry_exe), "env", "info", "--path"],
            cwd=str(script_dir),
            env=venv_env,
            check=False,  # Don't fail if poetry env info is not available
            capture_output=True,
            text=True,
        )
        if verify_result.returncode == 0:
            current_path = str(Path(verify_result.stdout.strip()).resolve())
            expected_path = str(Path(venv_dir.absolute()).resolve())
            if current_path != expected_path:
                print(
                    f"ERROR: Poetry switched to different virtual environment: {current_path}"
                )
                print(f"Expected: {expected_path}")
                sys.exit(1)
        # If poetry env info fails, we can't verify, but continue anyway
        # Poetry should still use the Python we specified via env use
        return True

    print("Running poetry lock...")
    verify_poetry_env()  # Verify before lock
    result = subprocess.run(
        [str(poetry_exe), "lock"],
        cwd=str(script_dir),
        env=venv_env,
        check=True,
        capture_output=True,
        text=True,
    )
    if result.stdout:
        print(result.stdout)
    if result.stderr:
        print(result.stderr)
    verify_poetry_env()  # Verify after lock

    print("Running poetry install...")
    verify_poetry_env()  # Verify before install
    result = subprocess.run(
        [str(poetry_exe), "install"],
        cwd=str(script_dir),
        env=venv_env,
        check=True,
        capture_output=True,
        text=True,
    )
    if result.stdout:
        print(result.stdout)
    if result.stderr:
        print(result.stderr)
    verify_poetry_env()  # Verify after install

    # Verify installation by checking if key packages are installed
    # This is critical - if packages aren't installed, PyInstaller won't find them
    print("Verifying package installation...")
    test_packages = ["torch", "transformers", "tokenizers"]
    missing_packages = []
    for package in test_packages:
        test_result = subprocess.run(
            [str(venv_python), "-c", f"import {package}; print({package}.__version__)"],
            capture_output=True,
            text=True,
            check=False,
        )
        if test_result.returncode == 0:
            version = test_result.stdout.strip()
            print(f"  ✓ {package} {version} installed")
        else:
            error_msg = (
                test_result.stderr.strip() if test_result.stderr else "Unknown error"
            )
            print(f"  ✗ {package} NOT found in virtual environment: {error_msg}")
            missing_packages.append(package)

    if missing_packages:
        print(
            f"\nERROR: Required packages are missing from virtual environment: {', '.join(missing_packages)}"
        )
        print("This indicates that poetry did not install dependencies correctly.")
        print("Please check the poetry install output above for errors.")
        sys.exit(1)

    print("Dependencies installed successfully")


def check_pyinstaller(venv_python):
    """
    Check if PyInstaller is installed.

    PyInstaller should be installed via poetry install from pyproject.toml.
    If it's missing, it means poetry install failed or didn't complete.
    """
    try:
        result = subprocess.run(
            [
                str(venv_python),
                "-c",
                "import PyInstaller; print(PyInstaller.__version__)",
            ],
            capture_output=True,
            text=True,
            check=True,
        )
        version = result.stdout.strip()
        print(f"PyInstaller version: {version}")
        return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        print("ERROR: PyInstaller is not installed in the virtual environment")
        print("PyInstaller should be installed via poetry install from pyproject.toml")
        print(
            "This indicates that poetry install may have failed or didn't complete correctly."
        )
        return False


def build():
    """
    Execute the complete build process.

    Steps:
    1. Setup virtual environment (outside project directory)
    2. Update pip and install 2.2.1 poetry
    3. Install project dependencies (including PyInstaller from pyproject.toml)
    4. Build executable using PyInstaller
    """
    script_dir = Path(__file__).parent

    venv_dir = setup_venv()
    venv_python = get_venv_python(venv_dir)

    update_pip(venv_python)
    install_poetry(venv_python)
    install_dependencies(venv_python, venv_dir, script_dir)

    if not check_pyinstaller(venv_python):
        sys.exit(1)

    print("=" * 50)
    print("IoTDB AINode PyInstaller Build Script")
    print("=" * 50)
    print()

    print("Starting build...")
    print()

    spec_file = script_dir / "ainode.spec"
    if not spec_file.exists():
        print(f"Error: Spec file not found: {spec_file}")
        sys.exit(1)

    # Set up environment for PyInstaller
    # When using venv_python, PyInstaller should automatically detect the virtual environment
    # and use its site-packages. We should NOT manually add site-packages to pathex.
    pyinstaller_env = get_venv_env(venv_dir)

    # Verify we're using the correct Python
    python_prefix_result = subprocess.run(
        [str(venv_python), "-c", "import sys; print(sys.prefix)"],
        capture_output=True,
        text=True,
        check=True,
    )
    python_prefix = python_prefix_result.stdout.strip()
    print(f"Using Python from: {python_prefix}")

    # Ensure PyInstaller runs from the virtual environment
    # The venv_python should automatically set up the correct environment
    cmd = [
        str(venv_python),
        "-m",
        "PyInstaller",
        "--noconfirm",
        str(spec_file),
    ]

    try:
        subprocess.run(cmd, check=True, env=pyinstaller_env)
    except subprocess.CalledProcessError as e:
        print(f"\nError: Build failed: {e}")
        sys.exit(1)

    print()
    print("=" * 50)
    print("Build completed!")
    print("=" * 50)
    print()
    print("Executable location: dist/ainode/ainode")
    print()
    print("Usage:")
    print("  ./dist/ainode/ainode start   # Start AINode")
    print()


if __name__ == "__main__":
    build()
